/*
package com.ainote.service.impl;

import com.ainote.service.AsrService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

*/
/**
 * Baidu 长语音异步识别服务
 *//*

@Service("baiduAsrService")
public class BaiduAsrService implements AsrService {

    private static final Logger logger = LoggerFactory.getLogger(BaiduAsrService.class);

    // 注入 Ngrok URL
    @Value("${NGROK_BASE_URL}")
    private String ngrokBaseUrl;

    // 确保注入静态文件路径前缀，以防它被硬编码
    // 根据您之前的日志，文件路径前缀是 /uploads/merge/
    // 假设它也通过配置读取（如果不是，请硬编码或添加到 application.properties/yaml）
    @Value("${asr.baidu.url_prefix:/uploads/merge/}")
    private String fileUrlPrefix;

    @Value("${asr.baidu.api_key:}")
    private String apiKey;

    @Value("${asr.baidu.secret_key:}")
    private String secretKey;

    @Value("${asr.baidu.merge_dir:./uploads/merge}")
    private String mergeDir;

    @Value("${asr.baidu.file_host_prefix:}")
    private String fileHostPrefix;

    private final OkHttpClient http = new OkHttpClient();
    private final ObjectMapper mapper = new ObjectMapper();

    private String cachedToken = null;
    private Instant tokenExpireAt = Instant.EPOCH;

    @Override
    public String transcribe(MultipartFile file) throws Exception {
        String filename = file.getOriginalFilename();
        if (file == null || file.isEmpty()) {
            logger.warn("上传文件为空");
            return "";
        }

        String originalFilename = file.getOriginalFilename();
        File tmpDir = new File(mergeDir);

        // 【修改部分】在保存文件前，确保目标目录存在，并检查创建是否成功。
        if (!tmpDir.exists()) {
            logger.info("文件上传目标目录不存在，尝试创建: {}", tmpDir.getAbsolutePath());
            // mkdirs() 会创建所有不存在的父目录
            if (!tmpDir.mkdirs()) {
                String errorMsg = "无法创建文件合并目录: " + tmpDir.getAbsolutePath();
                logger.error(errorMsg);
                // 抛出 IOException 停止后续操作
                throw new IOException(errorMsg);
            }
        }

        File tmpFile = new File(tmpDir, originalFilename);
        // 现在目录已确保存在，可以安全地传输文件
        file.transferTo(tmpFile);
        logger.info("保存上传文件到: {}", tmpFile.getAbsolutePath());

        // 构造可访问 URL
        String audioUrl = ngrokBaseUrl + fileUrlPrefix + filename;
        if (!audioUrl.startsWith("http")) {
            if (fileHostPrefix == null || fileHostPrefix.isBlank()) {
                throw new IllegalStateException("asr.baidu.file_host_prefix 未配置，无法构造可访问音频 URL");
            }
            String relativePath = tmpFile.getAbsolutePath();

            // 确保 relativePath 是相对路径或至少不是绝对路径的完整表示，这里使用 substring(1) 可能是为了去除潜在的 /
            // 为了安全起见，这里假设文件路径处理遵循之前的逻辑：
            // 尝试去除开头的 / 或 \，使其成为相对于 fileHostPrefix 的路径
            if (relativePath.startsWith("/")) relativePath = relativePath.substring(1);
            else if (relativePath.startsWith("\\")) relativePath = relativePath.substring(1);

            // 由于 tmpFile.getAbsolutePath() 返回的是绝对路径 (如 C:\...)，
            // 这里的逻辑可能需要更精细地处理，但我们先按原逻辑尝试构造 URL。
            // 如果文件路径是绝对路径，且 fileHostPrefix 是一个 URL，拼接会失败。
            //
            // 假设 mergeDir 是相对路径 (如 ./uploads/merge)，且 Tomcat 把它解析成一个绝对路径。
            // 此时需要从绝对路径中截取出相对于应用根目录的部分。
            // 由于这里不清楚您的文件服务是如何配置的，我们先保持原有的 URL 构造逻辑，
            // 重点是修复文件保存。


            audioUrl = fileHostPrefix + originalFilename; // 简化 URL 构造，假设前端通过文件名访问

            // 💡 提示: 如果上述简化版 audioUrl 仍有问题，建议检查您的前端配置或文件服务配置，
            // 以确定如何正确构造一个可被百度云访问的 HTTP/HTTPS URL。
        }
        logger.info("使用音频 URL: " + audioUrl);

        // ... 剩余部分保持不变 ...
        // 创建任务
        String taskId = createTask(audioUrl);
        logger.info("已创建百度长语音识别任务, taskId={}", taskId);

        // 轮询任务状态
        Map<String, Object> queryResult;
        int retries = 0;
        int maxRetries = 60; // 最多轮询 60 次，每次间隔 1 秒
        while (true) {
            queryResult = queryTask(taskId);
            int status = (int) queryResult.get("status");
            logger.info("轮询 taskId={} 状态: {}", taskId, status);
            if (status == 2) {
                logger.info("任务完成, 开始解析结果");
                break;
            } else if (status == 3) {
                logger.error("任务失败, taskId={}", taskId);
                throw new RuntimeException("百度长语音识别任务失败, taskId=" + taskId);
            }
            Thread.sleep(1000);
            retries++;
            if (retries >= maxRetries) {
                logger.warn("轮询超时, taskId={}", taskId);
                throw new RuntimeException("百度长语音识别轮询超时, taskId=" + taskId);
            }
        }

        String resultText = (String) queryResult.get("result");
        logger.info("识别结果: {}", resultText);

        return resultText;
    }

    public String createTask(String relativeOrFullFilePath) throws Exception {
        String audioUrl = relativeOrFullFilePath;
        if (!audioUrl.startsWith("http")) {
            if (fileHostPrefix == null || fileHostPrefix.isBlank()) {
                throw new IllegalStateException("asr.baidu.file_host_prefix 未配置");
            }
            if (audioUrl.startsWith("/")) audioUrl = audioUrl.substring(1);
            audioUrl = fileHostPrefix + audioUrl;
        }

        String token = getAccessToken();
        String apiUrl = "https://aip.baidubce.com/rpc/2.0/aasr/v1/create?access_token=" + token;

        Map<String, Object> body = new HashMap<>();
        body.put("speech_url", audioUrl);
        body.put("format", detectFormatFromUrl(audioUrl));
        body.put("rate", 16000);
        body.put("channel", 1);
        body.put("pid", 1537);

        String json = mapper.writeValueAsString(body);
        Request req = new Request.Builder()
                .url(apiUrl)
                .post(RequestBody.create(json, MediaType.parse("application/json")))
                .build();

        try (Response resp = http.newCall(req).execute()) {
            if (!resp.isSuccessful()) throw new IOException("createTask HTTP error: " + resp.code());
            String respStr = resp.body().string();
            JsonNode root = mapper.readTree(respStr);
            if (root.has("task_id")) return root.get("task_id").asText();
            else throw new IOException("创建任务失败: " + respStr);
        }
    }

    public Map<String, Object> queryTask(String taskId) throws Exception {
        String token = getAccessToken();
        String apiUrl = "https://aip.baidubce.com/rpc/2.0/aasr/v1/query?access_token=" + token;

        Map<String, Object> body = Map.of("task_ids", new String[]{taskId});
        String json = mapper.writeValueAsString(body);
        Request req = new Request.Builder()
                .url(apiUrl)
                .post(RequestBody.create(json, MediaType.parse("application/json")))
                .build();

        try (Response resp = http.newCall(req).execute()) {
            if (!resp.isSuccessful()) throw new IOException("queryTask HTTP error: " + resp.code());
            String respStr = resp.body().string();
            JsonNode root = mapper.readTree(respStr);
            JsonNode tasksInfo = root.path("tasks_info");
            if (!tasksInfo.isArray() || tasksInfo.size() == 0)
                throw new IOException("无效的任务查询响应: " + respStr);

            JsonNode task = tasksInfo.get(0);
            int status = task.path("task_status").asInt(0);
            String resultText = "";
            if (status == 2) {
                resultText = parseResultNode(task.path("result"));
            }
            Map<String, Object> out = new HashMap<>();
            out.put("status", status);
            out.put("result", resultText);
            out.put("raw", task);
            return out;
        }
    }

    private String parseResultNode(JsonNode resultNode) {
        if (resultNode == null || resultNode.isNull()) return "";
        StringBuilder sb = new StringBuilder();

        if (resultNode.isTextual()) sb.append(resultNode.asText());
        else if (resultNode.isArray()) {
            for (JsonNode item : resultNode) {
                if (item.isTextual()) sb.append(item.asText()).append("\n");
                else if (item.has("onebest")) sb.append(item.get("onebest").asText()).append("\n");
                else if (item.isObject()) {
                    if (item.has("serial")) sb.append(item.path("serial").asText()).append(" ");
                    if (item.has("result")) sb.append(item.path("result").asText()).append("\n");
                    else sb.append(item.toString()).append("\n");
                } else sb.append(item.toString()).append("\n");
            }
        } else sb.append(resultNode.toString());

        return sb.toString().trim();
    }

    private synchronized String getAccessToken() throws IOException {
        if (cachedToken != null && Instant.now().isBefore(tokenExpireAt)) return cachedToken;

        HttpUrl url = HttpUrl.parse("https://aip.baidubce.com/oauth/2.0/token")
                .newBuilder()
                .addQueryParameter("grant_type", "client_credentials")
                .addQueryParameter("client_id", apiKey)
                .addQueryParameter("client_secret", secretKey)
                .build();

        Request req = new Request.Builder().url(url).get().build();
        try (Response resp = http.newCall(req).execute()) {
            if (!resp.isSuccessful()) throw new IOException("获取 Access Token 失败: HTTP " + resp.code());
            String body = resp.body().string();
            JsonNode json = mapper.readTree(body);
            String token = json.path("access_token").asText(null);
            int expiresIn = json.path("expires_in").asInt(2592000);
            if (token == null) throw new IOException("access_token 字段缺失: " + body);
            cachedToken = token;
            tokenExpireAt = Instant.now().plusSeconds(Math.max(60, expiresIn - 60));
            return cachedToken;
        }
    }

    private String detectFormatFromUrl(String url) {
        String lower = url.toLowerCase();
        if (lower.endsWith(".wav")) return "wav";
        if (lower.endsWith(".pcm")) return "pcm";
        if (lower.endsWith(".mp3")) return "mp3";
        if (lower.endsWith(".m4a")) return "m4a";
        if (lower.endsWith(".amr")) return "amr";
        return "wav";
    }
}
*/

package com.ainote.service.impl;

import com.ainote.service.AsrService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

/**
 * Baidu 长语音异步识别服务
 */
@Service("baiduAsrService")
public class BaiduAsrService implements AsrService {

    private static final Logger logger = LoggerFactory.getLogger(BaiduAsrService.class);

    // 注入 Ngrok URL
    @Value("${NGROK_BASE_URL}")
    private String ngrokBaseUrl;

    @Value("${asr.baidu.url_prefix:/uploads/merge/}")
    private String fileUrlPrefix;

    @Value("${asr.baidu.api_key:}")
    private String apiKey;

    @Value("${asr.baidu.secret_key:}")
    private String secretKey;

    @Value("${asr.baidu.merge_dir:./uploads/merge}")
    private String mergeDir;

    @Value("${asr.baidu.file_host_prefix:}")
    private String fileHostPrefix;

    private final OkHttpClient http = new OkHttpClient();
    private final ObjectMapper mapper = new ObjectMapper();

    private String cachedToken = null;
    private Instant tokenExpireAt = Instant.EPOCH;

    /**
     * 新增方法：保存文件到本地并返回可供百度访问的 URL。
     * 这是原 transcribe() 方法中的文件保存和 URL 构造逻辑。
     * @param file 上传的 MultipartFile
     * @return 可供百度云访问的音频 URL
     */
    public String saveFileAndGetUrl(MultipartFile file) throws IOException, IllegalStateException {
        String originalFilename = file.getOriginalFilename();
        if (file == null || file.isEmpty() || originalFilename == null) {
            throw new IOException("上传文件为空或文件名缺失");
        }

        File tmpDir = new File(mergeDir);

        if (!tmpDir.exists()) {
            logger.info("文件上传目标目录不存在，尝试创建: {}", tmpDir.getAbsolutePath());
            if (!tmpDir.mkdirs()) {
                throw new IOException("无法创建文件合并目录: " + tmpDir.getAbsolutePath());
            }
        }

        File tmpFile = new File(tmpDir, originalFilename);
        // 现在目录已确保存在，可以安全地传输文件
        file.transferTo(tmpFile);
        logger.info("保存上传文件到: {}", tmpFile.getAbsolutePath());

        // 构造可访问 URL
        // 优先使用 ngrokBaseUrl (用于本地调试/内网穿透)
        String audioUrl = ngrokBaseUrl + fileUrlPrefix + originalFilename;
        if (!audioUrl.startsWith("http")) {
            // 如果 ngrokBaseUrl 未设置，则使用 fileHostPrefix (用于生产环境)
            if (fileHostPrefix == null || fileHostPrefix.isBlank()) {
                throw new IllegalStateException("asr.baidu.file_host_prefix 未配置，无法构造可访问音频 URL");
            }
            audioUrl = fileHostPrefix + originalFilename;
        }
        logger.info("使用音频 URL: " + audioUrl);
        return audioUrl;
    }

    /**
     * 新增方法：供 NoteController 调用的异步任务创建入口。
     * @param file 上传的 MultipartFile
     * @return 百度语音识别任务 ID
     */
    public String createAsrTask(MultipartFile file) throws Exception {
        String audioUrl = saveFileAndGetUrl(file);
        return createTask(audioUrl); // 调用 createTask，返回 taskId
    }

    /**
     * 【重要修改】原同步方法。由于长语音识别改为异步，此方法不再执行同步轮询。
     * 客户端应使用 createAsrTask 方法和 AsrController.getStatus 接口。
     */
    @Override
    public String transcribe(MultipartFile file) throws Exception {
        // 原本这里的同步轮询逻辑是导致超时的主要原因，现在已将其移除。
        logger.error("🚫 警告: 同步 transcribe(MultipartFile file) 方法已废弃，不再支持长语音识别。");
        throw new UnsupportedOperationException("同步 transcribe 方法已废弃，请使用 createAsrTask 创建异步任务并轮询状态。");
    }

    /**
     * 调用百度接口创建长语音识别任务
     */
    public String createTask(String relativeOrFullFilePath) throws Exception {
        String audioUrl = relativeOrFullFilePath;
        if (!audioUrl.startsWith("http")) {
            if (fileHostPrefix == null || fileHostPrefix.isBlank()) {
                throw new IllegalStateException("asr.baidu.file_host_prefix 未配置");
            }
            // 确保路径拼接正确
            if (audioUrl.startsWith("/")) audioUrl = audioUrl.substring(1);
            audioUrl = fileHostPrefix + audioUrl;
        }

        String token = getAccessToken();
        String apiUrl = "https://aip.baidubce.com/rpc/2.0/aasr/v1/create?access_token=" + token;

        Map<String, Object> body = new HashMap<>();
        body.put("speech_url", audioUrl);
        body.put("format", detectFormatFromUrl(audioUrl));
        body.put("rate", 16000);
        body.put("channel", 1);
        body.put("pid", 1537);

        String json = mapper.writeValueAsString(body);
        Request req = new Request.Builder()
                .url(apiUrl)
                .post(RequestBody.create(json, MediaType.parse("application/json")))
                .build();

        try (Response resp = http.newCall(req).execute()) {
            if (!resp.isSuccessful()) throw new IOException("createTask HTTP error: " + resp.code());
            String respStr = resp.body().string();
            JsonNode root = mapper.readTree(respStr);
            if (root.has("task_id")) return root.get("task_id").asText();
            else throw new IOException("创建任务失败: " + respStr);
        }
    }

    /**
     * 调用百度接口查询长语音识别任务状态
     * 此方法供 AsrController.getStatus() 接口调用
     */
    public Map<String, Object> queryTask(String taskId) throws Exception {
        String token = getAccessToken();
        String apiUrl = "https://aip.baidubce.com/rpc/2.0/aasr/v1/query?access_token=" + token;

        Map<String, Object> body = Map.of("task_ids", new String[]{taskId});
        String json = mapper.writeValueAsString(body);
        Request req = new Request.Builder()
                .url(apiUrl)
                .post(RequestBody.create(json, MediaType.parse("application/json")))
                .build();

        try (Response resp = http.newCall(req).execute()) {
            if (!resp.isSuccessful()) throw new IOException("queryTask HTTP error: " + resp.code());
            String respStr = resp.body().string();
            JsonNode root = mapper.readTree(respStr);
            JsonNode tasksInfo = root.path("tasks_info");
            if (!tasksInfo.isArray() || tasksInfo.size() == 0)
                throw new IOException("无效的任务查询响应: " + respStr);

            JsonNode task = tasksInfo.get(0);
            int status = task.path("task_status").asInt(0);
            String resultText = "";
            if (status == 2) {
                resultText = parseResultNode(task.path("result"));
            }
            Map<String, Object> out = new HashMap<>();
            out.put("status", status); // 0: 未开始, 1: 进行中, 2: 完成, 3: 失败
            out.put("result", resultText);
            out.put("raw", task);
            return out;
        }
    }

    private String parseResultNode(JsonNode resultNode) {
        if (resultNode == null || resultNode.isNull()) return "";
        StringBuilder sb = new StringBuilder();

        if (resultNode.isTextual()) sb.append(resultNode.asText());
        else if (resultNode.isArray()) {
            for (JsonNode item : resultNode) {
                if (item.isTextual()) sb.append(item.asText()).append("\n");
                else if (item.has("onebest")) sb.append(item.get("onebest").asText()).append("\n");
                else if (item.isObject()) {
                    if (item.has("serial")) sb.append(item.path("serial").asText()).append(" ");
                    if (item.has("result")) sb.append(item.path("result").asText()).append("\n");
                    else sb.append(item.toString()).append("\n");
                } else sb.append(item.toString()).append("\n");
            }
        } else sb.append(resultNode.toString());

        return sb.toString().trim();
    }

    private synchronized String getAccessToken() throws IOException {
        if (cachedToken != null && Instant.now().isBefore(tokenExpireAt)) return cachedToken;

        HttpUrl url = HttpUrl.parse("https://aip.baidubce.com/oauth/2.0/token")
                .newBuilder()
                .addQueryParameter("grant_type", "client_credentials")
                .addQueryParameter("client_id", apiKey)
                .addQueryParameter("client_secret", secretKey)
                .build();

        Request req = new Request.Builder().url(url).get().build();
        try (Response resp = http.newCall(req).execute()) {
            if (!resp.isSuccessful()) throw new IOException("获取 Access Token 失败: HTTP " + resp.code());
            String body = resp.body().string();
            JsonNode json = mapper.readTree(body);
            String token = json.path("access_token").asText(null);
            // 提前 60 秒过期，防止在临界点使用过期 Token
            int expiresIn = json.path("expires_in").asInt(2592000);
            if (token == null) throw new IOException("access_token 字段缺失: " + body);
            cachedToken = token;
            tokenExpireAt = Instant.now().plusSeconds(Math.max(60, expiresIn - 60));
            return cachedToken;
        }
    }

    private String detectFormatFromUrl(String url) {
        String lower = url.toLowerCase();
        if (lower.endsWith(".wav")) return "wav";
        if (lower.endsWith(".pcm")) return "pcm";
        if (lower.endsWith(".mp3")) return "mp3";
        if (lower.endsWith(".m4a")) return "m4a";
        if (lower.endsWith(".amr")) return "amr";
        return "wav";
    }
}